package chair.core;

import java.util.ArrayList;
import java.util.Hashtable;

public class CoreNoisyOrModel implements CoreModel {

	/*
	 * Attention conversion depuis NoisyOrBisModel TreeSet devient ArrayList
	 * TreeMap devient combo ArrayList + Hashtable Espérons que le tri
	 * des...Tree ne soit pas déterminant Niveau efficacité et clarté faudra
	 * repasser (créer nouvelle structure peut-être...)
	 */
	private static final long serialVersionUID = -7546582846304556256L;

	protected ArrayList<StimulusPair> listProbability;
	protected Hashtable stimulationProbability;
	protected Hashtable inhibitionProbability;
	protected ArrayList<SimulationStimulus> forecastList;
	protected Hashtable forecastStimulation;
	protected Hashtable forecastInhibition;

	protected ArrayList<SimulationStimulus> forecastStimuli;
	protected ArrayList<SimulationStimulus> memorizedStimuli;

	protected ArrayList<Stimulus> stimuliStrengh;
	protected Hashtable stimuliStrenghValue;

	protected boolean anyChange;

	protected String name;

	protected long time;

	// Settings
	/**
	 * Indique si les probabilités diminuent au fur et à mesure du temps
	 * 
	 */
	protected boolean decreasingStrengh;
	protected double decreasingStrenghSpeed;

	/**
	 * Le seuil à partir duquel un évènement est prévu.
	 */
	protected double forecastThreshold;

	/**
	 * alpha est le coefficient de correction d'erreur. Il doit être compris
	 * entre 0 et 1.
	 */
	protected double alpha;

	public CoreNoisyOrModel() {
		listProbability = new ArrayList<StimulusPair>();
		stimulationProbability = new Hashtable();
		inhibitionProbability = new Hashtable();
		forecastList = new ArrayList<SimulationStimulus>();
		forecastStimulation = new Hashtable();
		forecastInhibition = new Hashtable();
		stimuliStrengh = new ArrayList<Stimulus>();
		stimuliStrenghValue = new Hashtable();
		forecastStimuli = new ArrayList<SimulationStimulus>();
		alpha = 0.5;
		decreasingStrengh = true;
		decreasingStrenghSpeed = .99;
		memorizedStimuli = new ArrayList<SimulationStimulus>();
		forecastThreshold = .5;
		name = null;

	}

	@Override
	/**
	 * Attention, peut produire un résultat non attendu pour deux raison : 
	 *  - l'US est toujours actif (montant) dans l'environnement mais le système 
	 *    l'a oublié (> deltaMax)
	 *  - l'US s'est déroulé pendant un CS, la phase descendante produit le même 
	 *    effet qu'un noueau CS et peut appeler à elle toute seule l'US
	 */
	public synchronized boolean checkForecast(Stimulus checkedStimulus) {
		// parcours à l'envers les derniers stimulus actifs
		synchronized (memorizedStimuli) {
			for (SimulationStimulus previousStimulus : memorizedStimuli) {
				// S'il y a la trace du stimulus vérifié en mémoire
				if (previousStimulus.getStimulus().equals(checkedStimulus)) {
					// Si c'était le dernier stimulus ou est encore ascendant
					// alors
					// rien à prédire
					if (previousStimulus.equals(memorizedStimuli.get(0))
							|| previousStimulus.isRisingEdge())
						return false;
					// si est descendu alors peut en prédire un autre et passe
					// le test
					else
						break;
				}
			}
		}

		synchronized (forecastStimuli) {
			return forecastStimuli.contains(new SimulationStimulus(
					checkedStimulus, -1, true));
		}
	}

	@Override
	public synchronized void handleEvent(Stimulus stimulus, boolean risingEdge) {

		updateProbability(true, stimulus, risingEdge);

		// on ajoute le stimulus dans le tableau des stimuli mémorisé afin de
		// pouvoir l'utiliser par la suite
		// C'est dans la fonction tick que l'on signale la fin de la
		// mémorisation
		SimulationStimulus stim = new SimulationStimulus(stimulus, time,
				risingEdge);
		synchronized (memorizedStimuli) {
			int index = memorizedStimuli.indexOf(stim);
			if (index != -1) {
				memorizedStimuli.remove(index);
			}
			memorizedStimuli.add(0, stim);
		}

		// Diminution de la force associé au stimuli si nécessaire (option du
		// modèle)
		if (decreasingStrengh && risingEdge) {
			// stimuliStrengh.add(stimulus);
			stimuliStrenghValue.put(stimulus, (Double) stimuliStrenghValue
					.get(stimulus)
					* decreasingStrenghSpeed);

		}

		computeForecastProbability();
	}

	protected void computeForecastProbability() {

		synchronized (forecastList) {
			for (SimulationStimulus entry : forecastList) {
				forecastStimulation.put(entry, new Double(0));
				forecastInhibition.put(entry, new Double(0));
			}

			synchronized (listProbability) {
				for (StimulusPair key : listProbability) {
					synchronized (memorizedStimuli) {
						if (memorizedStimuli.indexOf(key.stimulusA) != -1) {
							// Stimulation
							Double temp = (Double) forecastStimulation
									.get(key.stimulusB);
							double newValue = temp.doubleValue();
							temp = (Double) stimulationProbability.get(key);
							double value = temp.doubleValue();
							newValue = 1 - newValue;
							newValue *= (1 - value);
							newValue = 1 - newValue;
							forecastStimulation.put(key.stimulusB, new Double(
									newValue));

							// Inhibition
							temp = (Double) forecastInhibition
									.get(key.stimulusB);
							newValue = temp.doubleValue();
							temp = (Double) inhibitionProbability.get(key);
							value = temp.doubleValue();
							newValue = 1 - newValue;
							newValue *= (1 - value);
							newValue = 1 - newValue;
							forecastInhibition.put(key.stimulusB, new Double(
									newValue));
						}
					}
				}
			}
		}

		// Déplacé hors de la boucle histoire de ne pas garder que le dernier
		// élément
		forecastStimuli = new ArrayList<SimulationStimulus>();
		synchronized (forecastList) {
			for (SimulationStimulus entry : forecastList) {
				double stimul = (Double) forecastStimulation.get(entry);
				double inhib = (Double) forecastInhibition.get(entry);
				if (stimul * (1 - inhib) > forecastThreshold)
					forecastStimuli.add(entry);
			}
		}
	}

	public synchronized ArrayList<Stimulus> checkForecastProbability(
			Stimulus stim, boolean risingEdge) {

		// Le stimulus qu'on va vérifier
		SimulationStimulus checkedStim = new SimulationStimulus(stim, -1,
				risingEdge);

		// Ici on travaille sur du théorique
		Hashtable tmpForecastStimulation = new Hashtable();
		Hashtable tmpForecastInhibition = new Hashtable();

		synchronized (forecastList) {
			for (SimulationStimulus entry : forecastList) {
				tmpForecastStimulation.put(entry, new Double(0));
				tmpForecastInhibition.put(entry, new Double(0));
			}
		}

		synchronized (listProbability) {
			for (StimulusPair key : listProbability) {
				// On ne garde de la liste des couples de stimuli du modèle que
				// ceux
				// qui nous préoccupent
				if (key.stimulusA.equals(checkedStim)) {
					// Stimulation
					Double temp = (Double) tmpForecastStimulation
							.get(key.stimulusB);
					double newValue = temp.doubleValue();
					temp = (Double) stimulationProbability.get(key);
					double value = temp.doubleValue();
					newValue = 1 - newValue;
					newValue *= (1 - value);
					newValue = 1 - newValue;
					tmpForecastStimulation.put(key.stimulusB, new Double(
							newValue));

					// Inhibition
					temp = (Double) tmpForecastInhibition.get(key.stimulusB);
					newValue = temp.doubleValue();
					temp = (Double) inhibitionProbability.get(key);
					value = temp.doubleValue();
					newValue = 1 - newValue;
					newValue *= (1 - value);
					newValue = 1 - newValue;
					tmpForecastInhibition.put(key.stimulusB, new Double(
							newValue));
				}
			}
		}

		ArrayList<Stimulus> checkedForecastStimuli = new ArrayList<Stimulus>();

		synchronized (forecastList) {
			for (SimulationStimulus entry : forecastList) {
				// On ne sélectionne que les fronts montants
				if (entry.isRisingEdge()) {
					double stimul = (Double) tmpForecastStimulation.get(entry);
					double inhib = (Double) tmpForecastInhibition.get(entry);
					if (stimul * (1 - inhib) > forecastThreshold) {
						// On va pas renvoyer directement à l'extérieur un
						// stimulus du modèle
						Stimulus tmpStim = (Stimulus) entry.getStimulus()
								.clone();
						checkedForecastStimuli.add(tmpStim);
					}
				}
			}
		}

		return checkedForecastStimuli;
	}

	private void handleEndOfEvent(Stimulus stimulus, boolean risingEdge) {
		// nouvel objet forecastStimulu sera créé dans
		// computeForecastProbability, donc inutile de cloner
		ArrayList<SimulationStimulus> backup = forecastStimuli;
		computeForecastProbability();

		// Des fois qu'un autre thread utilise encore ref de backup comme
		// forecastStimuli
		synchronized (backup) {
			synchronized (forecastStimuli) {
				// TODO: bug avec removeAll() dans leJOS 0.8.5
				// backup.removeAll(forecastStimuli);
				// On va faire ça à la main
				// cf implémentation
				// http://lejos.svn.sourceforge.net/viewvc/lejos/trunk/classes/java/util/AbstractCollection.java?revision=2393
				for (SimulationStimulus tmpStim : forecastStimuli) {
					backup.remove(tmpStim);
				}
			}
			synchronized (memorizedStimuli) {
				// TODO: bug avec isEmpty() dans leJOS 0.8.5
				// http://lejos.sourceforge.net/forum/viewtopic.php?t=1809
				if (!(backup.size() == 0)) {
					memorizedStimuli.add(new SimulationStimulus(stimulus, -1,
							risingEdge));

					for (SimulationStimulus stim : backup) {

						if (memorizedStimuli.indexOf(stim) == -1) {
							// Un évènement prévu n'est pas survenu, il faut
							// donc augmenter les probabilité d'inhibition qui
							// lui sont liées.
							// jfrey: et diminuer les probas de stimulation (oui
							// j'avais un doute sur la fonction)
							updateProbability(false, stim.getStimulus(), stim
									.isRisingEdge());
						}
					}
					memorizedStimuli.remove(memorizedStimuli.size() - 1);
				}
			}
		}
		anyChange = true;
	}

	/**
	 * Cette fonction permet de mettre à jour les probabilité, soit après un
	 * renforcement, soit après un non-renforcement. Seul un signe et le contenu
	 * du produit change entre ces deux cas, il est donc très intéressants de
	 * factoriser le code.
	 * 
	 * @param reinforce
	 *            Doit être vrai en cas de renforcement, faux sinon.
	 * @param stimulus
	 *            Le stimulus principal de cette mise à jour des probabilités
	 * @param risingEdge
	 *            Vrai si l'évènement est un front montant, faux sinon
	 */
	protected void updateProbability(boolean reinforce, Stimulus stimulus,
			boolean risingEdge) {
		double totalInhibition = 1;
		double totalStrengh = 1;
		synchronized (memorizedStimuli) {
			for (SimulationStimulus previousStimulus : memorizedStimuli) {
				StimulusPair stimPair = new StimulusPair(previousStimulus,
						new SimulationStimulus(stimulus, -1, risingEdge));

				Double linkStr = (Double) stimulationProbability.get(stimPair);
				Double inhibitStr = (Double) inhibitionProbability
						.get(stimPair);
				if (reinforce) {
					if (linkStr != null) {
						totalStrengh *= (1 - linkStr.doubleValue());
					}
					if (inhibitStr != null) {
						totalInhibition *= inhibitStr.doubleValue();
					} else {
						totalInhibition *= 0;
					}
				} else {
					if (linkStr != null) {
						totalStrengh *= linkStr.doubleValue();
					} else {
						totalStrengh *= 0;
					}
					if (inhibitStr != null) {
						totalInhibition *= 1 - inhibitStr.doubleValue();
					}
				}
			}
			for (SimulationStimulus previousStimulus : memorizedStimuli) {

				StimulusPair stimPair = new StimulusPair(previousStimulus,
						new SimulationStimulus(stimulus, -1, risingEdge));
				Double dLinkStr = (Double) stimulationProbability.get(stimPair);
				Double dInhibitStr = (Double) inhibitionProbability
						.get(stimPair);
				double linkStr;
				double newLinkStr;
				double inhibitStr;
				double newInhibitStr;

				if (dLinkStr != null) {
					linkStr = dLinkStr.doubleValue();
					newLinkStr = 1;
				} else {
					linkStr = 0;
					newLinkStr = 1;
				}

				if (dInhibitStr != null) {
					inhibitStr = dInhibitStr.doubleValue();
					newInhibitStr = 1;
				} else {
					inhibitStr = 0;
					newInhibitStr = 1;
				}

				double strenghtProduct = totalStrengh;
				double inhibitionProduct = totalInhibition;

				// Le stimulus dont on modifie les proba ne doit pas être pris
				// en
				// compte dans le produit.
				// if (linkStr != 1)
				// totalStrengh /= (1 - linkStr);
				// if (inhibitStr != 0)
				// inhibitionProduct /= inhibitStr;
				// En fait, elle doit être prise en compte dans le produit dans
				// l'état actuel du modele.

				newLinkStr *= strenghtProduct;
				newInhibitStr *= inhibitionProduct;

				newLinkStr *= (Double) stimuliStrenghValue.get(previousStimulus
						.getStimulus());
				newLinkStr *= (Double) stimuliStrenghValue.get(stimulus);
				newLinkStr *= alpha;

				newInhibitStr *= (Double) stimuliStrenghValue
						.get(previousStimulus.getStimulus());
				newInhibitStr *= (Double) stimuliStrenghValue.get(stimulus);
				newInhibitStr *= alpha;

				// newLinkStr contient Gamma_1
				// newInhibitStr contient Gamma_2
				// FIXME doit on faire le check différend de 1?
				if (inhibitStr != 1)
					newLinkStr *= (1 - inhibitStr);

				if (linkStr != 1)
					newInhibitStr *= (1 - linkStr);

				if (reinforce)
					newInhibitStr *= -1;
				else
					newLinkStr *= -1;

				newLinkStr += linkStr;
				newInhibitStr += inhibitStr;
				// Tous les termes de la formule ont été pris en compte, il faut
				// maintenant mettre a jour le total des forces (on divise par
				// l'ancien et on multplie par le nouveau, pour être prêt à
				// traiter le stimulus suivant ;))

				if (newLinkStr != linkStr || newInhibitStr != inhibitStr) {
					if (linkStr != 1) {
						totalStrengh /= (1 - linkStr);
						totalStrengh *= (1 - newLinkStr);
					}

					if (inhibitStr != 0) {
						totalInhibition /= inhibitStr;
						totalInhibition *= newInhibitStr;
					}

					// On ne veux pas deux StimulusPair identiques
					synchronized (listProbability) {
						if (!listProbability.contains(stimPair)) {
							listProbability.add(stimPair);
						}
					}

					stimulationProbability
							.put(stimPair, new Double(newLinkStr));
					inhibitionProbability.put(stimPair, new Double(
							newInhibitStr));

					// Mise a jour des prévision afin de que la bonne valeur
					// soit prise en compte lors du handleEndOfEvent
					anyChange = true;
				}
			}
		}
	}

	@Override
	public synchronized void plugStimulus(Stimulus stim) {
		synchronized (stimuliStrengh) {
			if (stimuliStrengh.contains(stim))
				throw new IllegalArgumentException(
						"Le modèle contient déjà ce stimulus");
			stimuliStrengh.add(stim);
			stimuliStrenghValue.put(stim, new Double(1));
		}

		// Initialise probas avec fronts montants
		SimulationStimulus tempStim = new SimulationStimulus(stim, -1, true);
		synchronized (forecastList) {
			forecastList.add(tempStim);
			forecastStimulation.put(tempStim, new Double(0));
			forecastInhibition.put(tempStim, new Double(0));
			// Fronts descendants
			tempStim = new SimulationStimulus(stim, -1, false);
			forecastList.add(tempStim);
			forecastStimulation.put(tempStim, new Double(0));
			forecastInhibition.put(tempStim, new Double(0));
		}
	}

	@Override
	public synchronized void initializeModel(ArrayList<Stimulus> stimuli) {
		// Pour l'instant on met toutes les forces à 1 :
		for (Stimulus stim : stimuli) {
			plugStimulus(stim);
		}
	}

	/**
	 * À surcharger dans version normale pour inclure sauvegarde résulats
	 * 
	 * @param t
	 *            : non sert à rien ici (mais classe fille utilisera dans
	 *            surcharge)
	 */
	protected void hasChanged(long t) {
		anyChange = false;
		return;
	}

	@Override
	public synchronized void tick(long t) {
		time = t;
		// VirginNoisyOrModel sauvegardera résultat
		// TODO: à mettre dans interface ?
		if (anyChange) {
			hasChanged(t);
		}

		synchronized (memorizedStimuli) {
			while (memorizedStimuli.size() > 0
					&& memorizedStimuli.get(memorizedStimuli.size() - 1)
							.getBeginTime()
							+ chair.core.Parameters.getDeltaMax() < time) {
				SimulationStimulus oldStim = memorizedStimuli
						.get(memorizedStimuli.size() - 1);
				memorizedStimuli.remove(memorizedStimuli.size() - 1);
				handleEndOfEvent(oldStim.getStimulus(), oldStim.isRisingEdge());
			}
		}
	}

	@Override
	public synchronized String debug() {
		String result = "";
		// Équivalent à Map getInhibitionProbability()
		result += "InhibitionProbability : ";
		synchronized (listProbability) {
			for (StimulusPair key : listProbability) {
				Double value = (Double) inhibitionProbability.get(key);
				result += key + "=" + value + ", ";
			}
			// Équivalent à Map getStimulationProbability()
			result += "\nStimulationProbability : ";
			for (StimulusPair key : listProbability) {
				Double value = (Double) stimulationProbability.get(key);
				result += key + "=" + value + ", ";
			}
			return result;
		}
	}

	public String toString() {
		return getName();
	}

	@Override
	public String getName() {
		if (name == null)
			return "Modèle Double OU bruité";
		else
			return name + " (double OU bruité)";
	}

	@Override
	public void setName(String newName) {
		name = newName;

	}

	public double getForecastThreshold() {
		return forecastThreshold;
	}

	public void setForecastThreshold(double forecastThreshold) {
		this.forecastThreshold = forecastThreshold;
	}

	public boolean isDecreasingStrengh() {
		return decreasingStrengh;
	}

	public void setDecreasingStrengh(boolean decreasingStrengh) {
		this.decreasingStrengh = decreasingStrengh;
	}

	public double getDecreasingStrenghSpeed() {
		return decreasingStrenghSpeed;
	}

	public void setDecreasingStrenghSpeed(double decreasingStrenghSpeed) {
		this.decreasingStrenghSpeed = decreasingStrenghSpeed;
	}

	public double getAlpha() {
		return alpha;
	}

	public void setAlpha(double alpha) {
		this.alpha = alpha;
	}

}
